import tkinter
import tkinter.messagebox
import tkinter.font
import tkinter.ttk
import threading
import copy
import time

# Name: Sudoku | 2018 - 2020
# Version: 5.0 (GUI)
# GUI version: 1.2 (Tkinter)
# Last update: 2020
# ----------------------------------------
# Developer: Monkeyhbd | 2017 - 2020
# Email: monkeyhbd@163.com
# ----------------------------------------
#
#                         ##
#                         ##    ##    ####    ##    ##    ##      ##          ##
#     ##  ##      ####    ##  ##    ##    ##    ##  ##    ##      ##          ##
#   ##  ##  ##  ##    ##  ####      ########      ##      ####    ####      ####
#   ##  ##  ##  ##    ##  ##  ##    ##            ##      ##  ##  ##  ##  ##  ##
#   ##  ##  ##    ####    ##    ##    ####      ##        ##  ##  ####      ####
#
# ----------------------------------------
# License: GNU GPLv2

#测试数据
test1_1 = '093102040'
test1_2 = '810905007'
test1_3 = '050000000'
test1_4 = '082004000'
test1_5 = '045090620'
test1_6 = '000200180'
test1_7 = '000000070'
test1_8 = '400708056'
test1_9 = '070501830'

test2_1 = '400008000'
test2_2 = '502040000'
test2_3 = '381000507'
test2_4 = '903080020'
test2_5 = '007459600'
test2_6 = '060020705'
test2_7 = '708000954'
test2_8 = '000090302'
test2_9 = '000300008'

test3_1 = '089006070'
test3_2 = '000400900'
test3_3 = '601500300'
test3_4 = '105008003'
test3_5 = '800307005'
test3_6 = '900100407'
test3_7 = '003004106'
test3_8 = '002005000'
test3_9 = '060900740'

test4_1 = '106004073'
test4_2 = '270050010'
test4_3 = '000300008'
test4_4 = '800000300'
test4_5 = '020000080'
test4_6 = '004000002'
test4_7 = '300001000'
test4_8 = '060020059'
test4_9 = '490500201'

test5_1 = '006020007'
test5_2 = '700016820'
test5_3 = '000000054'
test5_4 = '094070000'
test5_5 = '020003760'
test5_6 = '000000000'
test5_7 = '180000000'
test5_8 = '300008400'
test5_9 = '069051008'

test6_1 = '000700005'
test6_2 = '000000060'
test6_3 = '010003000'
test6_4 = '080000100'
test6_5 = '500600007'
test6_6 = '000090000'
test6_7 = '306000002'
test6_8 = '000000000'
test6_9 = '000018900'


class Public:
    Enumeration_break_token = False#当点击停止按钮时，该值变为True，穷举算法被迫退出，Enumeration返回False
    Run_Button_mode = True#True - 显示 ▶，False - 显示 ■


class Sudoku:
    
    #例如：input: [[1],[8],[6],[2],[7],[3,9],[9],[4],[5,9]] , return: [[1],[8],[6],[2],[7],[3],[9],[4],[5]]
    def Exclusion_unit(sudoku_unit):
        while True:
            sum0 = 0
            for x in range(9):
                sum0 += len(sudoku_unit[x])

            for x in sudoku_unit:
                if len(x) == 1:
                    key = x[0]
                    for y in range(9):
                        if key in sudoku_unit[y] and len(sudoku_unit[y]) != 1:
                            sudoku_unit[y].remove(key)

            sum1 = 0
            for x in range(9):
                sum1 += len(sudoku_unit[x])

            if sum1 == sum0:
                break

        return sudoku_unit

    #例如：input: [[1],[8],[6],[2],[7],[3,9],[5,9],[4],[5,9]] , return: [[1],[8],[6],[2],[7],[3],[5,9],[4],[5,9]]
    def Look_unit(sudoku_unit):
        else_number = []
        for p in sudoku_unit:
            if len(p) != 1:
                else_number += p
        for n in range(1,10):
            if else_number.count(n) == 1:
                for x in range(9):
                    if n in sudoku_unit[x]:
                        sudoku_unit[x] = [n]
        return sudoku_unit

    #检查sudoku_unit中已填入的数字中有没有相同数字，有则返回False，无则返回True
    #例如：input: [[5, 9], [4], [5, 9], [3], [1], [7], [6], [2], [8]] , return True
    #例如：input: [[5, 9], [4], [5, 9], [3], [7], [7], [6], [2], [8]] , return False
    def Check_unit(sudoku_unit):
        for p in sudoku_unit:
            if len(p) == 1 and sudoku_unit.count(p) > 1:
                return False
        else:
            return True

    def Check(sudoku):
        for y in range(9):#行检查
            if not Sudoku.Check_unit(sudoku[y]):
                return False
        for x in range(9):#列检查
            if not Sudoku.Check_unit([sudoku[y][x] for y in range(9)]):
                return False
        for b in range(9):#宫排除
            help_place = []
            for x in range((b%3)*3 , (b%3)*3+3):
                for y in range(int(b/3)*3 , int(b/3)*3+3):
                    help_place.append([x , y])
            if not Sudoku.Check_unit([sudoku[p[1]][p[0]] for p in help_place]):
                return False
        return True

    def Count(sudoku):
        c = 0
        for y in range(9):
            for x in range(9):
                c += len(sudoku[y][x])
        return c

    #集成了Exclusion_unit和Look_unit
    def Exclusion(sudoku):
        while True:
            sum0 = 0
            for x in range(9):
                for y in range(9):
                    sum0 += len(sudoku[y][x])
            
            for y in range(9):#行排除
                sudoku[y] = Sudoku.Exclusion_unit(sudoku[y])
                sudoku[y] = Sudoku.Look_unit(sudoku[y])

            for x in range(9):#列排除
                help_unit = Sudoku.Exclusion_unit([sudoku[y][x] for y in range(9)])
                help_unit = Sudoku.Look_unit(help_unit)
                for y in range(9):
                    sudoku[y][x] = help_unit[y]

            for b in range(9):#宫排除
                help_place = []
                for x in range((b%3)*3 , (b%3)*3+3):
                    for y in range(int(b/3)*3 , int(b/3)*3+3):
                        help_place.append([x , y])
                help_unit = Sudoku.Exclusion_unit([sudoku[p[1]][p[0]] for p in help_place])
                help_unit = Sudoku.Look_unit(help_unit)
                for idx in range(9):
                    sudoku[help_place[idx][1]][help_place[idx][0]] = help_unit[idx]

            sum1 = 0
            for x in range(9):
                for y in range(9):
                    sum1 += len(sudoku[y][x])

            if sum1 == sum0:
                break
        
        return sudoku

    #Enumeration根据程序需要有修改
    def Enumeration(sudoku , empty = [] , idx = 0 , key = []):
        if idx == 0:
            key = copy.deepcopy(sudoku)
            empty = []
            Public.Enumeration_break_token = False
            for y in range(9):
                for x in range(9):
                    if len(sudoku[y][x]) > 1:
                        empty.append([x , y])

        if Public.Enumeration_break_token:
            return False

        for n in sudoku [empty[idx][1]] [empty[idx][0]]:
            key [empty[idx][1]] [empty[idx][0]] = [n]
            if idx == len(empty) - 1:
                if Sudoku.Check(key):
                    return key
            else:
                if Sudoku.Check(key):
                    help_return = Sudoku.Enumeration(sudoku , empty , idx+1 , [a.copy() for a in key])
                    if help_return != None:
                        return help_return

    #根据程序需要有修改
    def Solution(sudoku):
        sudoku = Sudoku.Exclusion(sudoku)

        if Sudoku.Count(sudoku) > 81:
            GUI_command.entry_disable()
            progressbar.place(x = 240 , y = 215 , width = 60 , height = 10)
            progressbar.start()
            
            sudoku = Sudoku.Enumeration(sudoku)
            
            progressbar.stop()
            progressbar.place_forget()
            GUI_command.entry_normal()

        return sudoku


class GUI:
    def win_init():
        globals()['lmy'] = tkinter.Tk()
        lmy.geometry('690x370')
        lmy.resizable(False, False)
        lmy.title('解数独 5.0')
    
    def menu_init():
        allmenu = tkinter.Menu(lmy)

        testbox = tkinter.Menu(lmy , tearoff = 0)
        testbox.add_command(label = '测试1 - 简单' , command = lambda:GUI_command.auto_input(1))
        testbox.add_command(label = '测试2 - 简单' , command = lambda:GUI_command.auto_input(2))
        testbox.add_command(label = '测试3 - 简单' , command = lambda:GUI_command.auto_input(3))
        testbox.add_separator()
        testbox.add_command(label = '测试4 - 中等' , command = lambda:GUI_command.auto_input(4))
        testbox.add_separator()
        testbox.add_command(label = '测试5 - 复杂' , command = lambda:GUI_command.auto_input(5))
        testbox.add_separator()
        uneasybox = tkinter.Menu(lmy , tearoff = 0)
        uneasybox.add_command(label = '测试6 - 很复杂' , command = lambda:GUI_command.auto_input_uneasy(6))
        testbox.add_cascade(label = '很复杂' , menu =uneasybox)

        operationbox = tkinter.Menu(lmy , tearoff = 0)
        operationbox.add_command(label = '清空' , command = lambda:GUI_command.all_clear())

        helpbox = tkinter.Menu(lmy , tearoff = 0)
        helpbox.add_command(label = '帮助文档' , command = lambda:GUI_command.helpf())

        aboutbox = tkinter.Menu(lmy , tearoff = 0)
        aboutbox.add_command(label = '关于解数独' , command = lambda:GUI_command.aboutf())
        aboutbox.add_command(label = '关于开发者' , command = lambda:GUI_command.writerf())

        allmenu.add_cascade(label = '测试' , menu = testbox)
        allmenu.add_cascade(label = '操作' , menu = operationbox)
        allmenu.add_cascade(label = '帮助' , menu = helpbox)
        allmenu.add_cascade(label = '更多' , menu = aboutbox)

        lmy.config(menu=allmenu)

    def font_init():
        globals()['ft'] = tkinter.font.Font(family='微软雅黑', size=18, weight=tkinter.font.BOLD)
        globals()['ft2'] = tkinter.font.Font(family='微软雅黑', size=15)
        globals()['ft3'] = tkinter.font.Font(size=15)

    def content_init():
        #界面初始化
        for user_input_n in range(1,10):
            globals()['user_input' + str(user_input_n)] = tkinter.StringVar()

        #a,b
        black = tkinter.Label(lmy , bg = 'black').place(x = 313 , y = 8 , width = 353 , height = 353)

        for a in range(1,10):
            for b in range(1,10):
                globals()['a_output' + str(a) + str(b)] = tkinter.Label(lmy , font = ft , text = '' , bg = 'white')
                globals()['a_output' + str(a) + str(b)].place(x = 280 + b*38 + int((b-1)/3)*2 , y = -25 + a*38 + int((a-1)/3)*2 , width = 35 , height = 35)

        globals()['Run_Button'] = tkinter.Button(lmy , text = '▶' , font = ft3 , command = lambda:GUI_command.Run_Button_command_td())
        Run_Button.place(x = 255 , y = 150 , width = 30 , height = 30)

        globals()['time_label'] = tkinter.Label(lmy , text = '' , bg = 'white')
        time_label.place(x = 240 , y = 190 , width = 40 , height = 15)

        globals()['time_label2'] = tkinter.Label(lmy , text = '' , bg = 'white')
        time_label2.place(x = 285 , y = 190 , width = 15 , height = 15)

        globals()['progressbar'] = tkinter.ttk.Progressbar(lmy , maximum = 30 , mode = 'indeterminate')

        for a_n in range(1,10):
            globals()['a_label' + str(a_n)] = tkinter.Label(lmy , font = ft2 , text = 'Line' + str(a_n) + '：').place(x = 5 , y = - 27 + 38*a_n + int((a_n-1)/3)*2)
            globals()['a' + str(a_n)] = tkinter.Entry(lmy , font = ft2 , textvariable = globals()['user_input' + str(a_n)])
            globals()['a' + str(a_n)].place(x = 75 , y = - 25 + 38*a_n + int((a_n-1)/3)*2 , width = 150 , height = 30)

    def display():
        GUI.win_init()
        GUI.menu_init()
        GUI.font_init()
        GUI.content_init()


class GUI_command:
    def Run_Button_command():
        globals()['sudoku_data'] = []

        try:
            for y in range(9):
                sudoku_unit_str = globals()['user_input' + str(y+1)].get()
                globals()['sudoku_data'].append([])
                for x in range(9):
                    if sudoku_unit_str[x] == '0':
                        globals()['sudoku_data'][y].append([1,2,3,4,5,6,7,8,9])
                    else:
                        globals()['sudoku_data'][y].append([int(sudoku_unit_str[x])])
        except ValueError:#4.输入非数字
            GUI_command.place_clear()
            a = tkinter.messagebox.showwarning('错误' , '你应该输入数字')
            GUI_command.change_Run_Button()
            GUI_command.entry_normal()
            return None
        except IndexError:#1.输入不完整
            GUI_command.place_clear()
            a = tkinter.messagebox.showwarning('错误' , '请检查你的输入')
            GUI_command.change_Run_Button()
            GUI_command.entry_normal()
            return None

        t0 = time.time()

        help_sudoku = Sudoku.Solution(globals()['sudoku_data'])

        te = time.time()

        if help_sudoku == False:#点击停止而退出
            return None

        if help_sudoku == None:#2.输入重复：（调用了穷举法）
            GUI_command.place_clear()
            a = tkinter.messagebox.showwarning('错误' , '请检查你的输入')
            GUI_command.change_Run_Button()
            GUI_command.entry_normal()
            return None

        if Sudoku.Check(help_sudoku) == False:#3.输入重复：（没有调用穷举法）
            GUI_command.place_clear()
            a = tkinter.messagebox.showwarning('错误' , '请检查你的输入')
            GUI_command.change_Run_Button()
            GUI_command.entry_normal()
            return None

                    
        globals()['sudoku_data'] = help_sudoku

        
        GUI_command.show_time(te - t0)

        for a in range(1,10):
            for b in range(1,10):
                globals()['a_output' + str(a) + str(b)]['text'] = sudoku_data[a-1][b-1][0]
                globals()['a_output' + str(a) + str(b)]['fg'] = 'black'

        #原数独的没有数字，用红色
        for a in range(1,10):
            for b in range(1,10):
                if globals()['user_input' + str(a)].get()[b-1] == '0':
                    globals()['a_output' + str(a) + str(b)]['fg'] = 'red'

        GUI_command.change_Run_Button()

    def Run_Button_command_td():
        if Public.Run_Button_mode:# == True
            GUI_command.change_Run_Button()
            td = threading.Thread(target = GUI_command.Run_Button_command)
            td.setDaemon(True)
            td.start()
        else:
            a = tkinter.messagebox.askyesno('提示' , '你确定要停止吗？')
            if a:
                GUI_command.change_Run_Button()
                Public.Enumeration_break_token = True

    def change_Run_Button():
        if Public.Run_Button_mode:# == True
            Public.Run_Button_mode = False
            Run_Button['text'] = '■'
        else:
            Public.Run_Button_mode = True
            Run_Button['text'] = '▶'

    def auto_input(test_n):
        if Public.Run_Button_mode:
            GUI_command.all_clear()
            for test_n_m in range(1,10):
                globals()['user_input' + str(test_n_m)].set(globals()['test' + str(test_n) + '_' + str(test_n_m)])
        else:
            a = tkinter.messagebox.showinfo('提示' , '请先停止')

    def auto_input_uneasy(test_n):
        if Public.Run_Button_mode:
            GUI_command.all_clear()
            for test_n_m in range(1,10):
                globals()['user_input' + str(test_n_m)].set(globals()['test' + str(test_n) + '_' + str(test_n_m)])
            askstr = '这些数独要花费很长时间，你确定要继续吗？'
            a = tkinter.messagebox.askyesno('提示' , askstr)
            if not a:
                GUI_command.all_clear()
        else:
            a = tkinter.messagebox.showinfo('提示' , '请先停止')

    def show_time(t):
        if t < 60:
            time_label['text'] = str(t)[:5]
            time_label2['text'] = 's'
        if t > 60 and t <3600:
            time_label['text'] = str(t/60)[:5]
            time_label2['text'] = 'm'
        if t > 3600:
            time_label['text'] = str(t/3600)[:5]
            time_label2['text'] = 'h'

    def all_clear():
        if Public.Run_Button_mode:
            for user_input_n in range(1,10):
                globals()['user_input' + str(user_input_n)].set('')
            GUI_command.place_clear()
        else:
            a = tkinter.messagebox.showinfo('提示' , '请先停止')

    def place_clear():
        for a in range(1,10):
            for b in range(1,10):
                globals()['a_output' + str(a) + str(b)]['text'] = ''
        time_label['text'] = ''
        time_label2['text'] = ''

    def entry_disable():
        for a in range(1,10):
            globals()['a' + str(a)]['state'] = 'disable'

    def entry_normal():
        for a in range(1,10):
            globals()['a' + str(a)]['state'] = 'normal'

    def aboutf():
        aboutstr = '版本：Version 5.0\nGUI版本：Version 1.2（tkinter）\n新内容：'
        aboutstr += '\n  -Version 5.0'
        aboutstr += '\n    -重写核心算法，速度提升约5倍'
        a = tkinter.messagebox.showinfo('版本信息' , aboutstr)

    def writerf():
        a = tkinter.messagebox.showinfo('关于开发者' , '作者：Monkeyhbd\n邮箱：monkeyhbd@163.com')

    def helpf():
        helpstr = '-我该怎么使用：'
        helpstr += '\n    找到一个正确的数独题，一行一行的输入，用 0 占位表示一个空格。'
        helpstr += '\n'
        helpstr += '\n-为什么程序长时间没有显示答案：'
        helpstr += '\n    部分复杂的数独，尤其是那些看上去很空旷的，程序可能需要花费很长时间来计算，你可以点击 ■ 来停止计算。'
        a = tkinter.messagebox.showinfo('帮助' , helpstr)


if __name__ == '__main__':
    GUI.display()
    lmy.mainloop()
